/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ArkFile, AstTreeUtils, ts} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData} from "../BaseChecker";
import {FileMatcher, MatcherCallback, MatcherTypes} from "../../matcher/Matchers";
import {Defects, IssueReport} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {RuleListUtil} from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoUnsafeCallCheck');
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/no-unsafe-call-check.md",
    description: "Disallow calling a function with a value with type `any`"
};

interface PositionInfo {
    startPosition: ts.LineAndCharacter;
    endPosition: ts.LineAndCharacter;
}

export class NoUnsafeCallCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatchBuildCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        }
        return [fileMatchBuildCb];
    }

    public check = (arkFile: ArkFile): void => {
        if (!arkFile.getFilePath().endsWith(".ts")) {
            return;
        }
        const code = arkFile.getCode();
        if (!code) {
            return;
        }
        const asRoot = AstTreeUtils.getASTNode(arkFile.getName(), code);
        const sourceFileObject = ts.getParseTreeNode(asRoot);
        if (sourceFileObject === undefined) {
            return;
        }
        this.loopNode(arkFile, asRoot, sourceFileObject);
    }

    public loopNode(targetFile: ArkFile, sourceFile: ts.SourceFile, aNode: ts.Node): void {
        const children = aNode.getChildren();
        for (const child of children) {
            if (ts.isFunctionDeclaration(child)) {
                this.checkFunctionDeclaration(child, sourceFile, targetFile);
            }
            if (ts.isExpressionStatement(child)) {
                this.checkExpressionStatement(child, sourceFile, targetFile);
            }
            this.loopNode(targetFile, sourceFile, child);
        }
    }

    private checkFunctionDeclaration(child: ts.FunctionDeclaration, sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        const body = child.body;
        if (!body || !ts.isBlock(body)) {
            return;
        }
        for (const statement of body.statements) {
            if (ts.isExpressionStatement(statement)) {
                this.handleExpressionStatement(child, statement, sourceFile, arkFile);
            }
        }
    }

    private handleExpressionStatement(child: ts.FunctionDeclaration, statement: ts.ExpressionStatement,
                                      sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        if (ts.isCallExpression(statement.expression)) {
            this.handleCallExpression(child, statement.expression, sourceFile, arkFile);
        }
        if (ts.isNewExpression(statement.expression)) {
            this.handleNewExpression(child, statement.expression, sourceFile, arkFile);
        }
        if (ts.isTaggedTemplateExpression(statement.expression)) {
            this.handleTaggedTemplateExpression(child, statement.expression, sourceFile, arkFile);
        }
    }

    private handleCallExpression(child: ts.FunctionDeclaration, expression: ts.CallExpression,
                                 sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        if (ts.isIdentifier(expression.expression)) {
            let positionInfo = this.getPositionInfo(expression.expression, sourceFile);
            let name = expression.expression.getText();
            this.checkParameter(child, name, positionInfo, arkFile);
        }
        if (ts.isPropertyAccessExpression(expression.expression)) {
            let positionInfo = this.getPositionInfo(expression.expression, sourceFile);
            let className = this.getLastExpression(expression.expression).getText();
            let name = expression.expression.name.getText();
            this.checkPropertyAccessParameter(child, className, name, positionInfo, arkFile);
        }
    }

    private handleNewExpression(child: ts.FunctionDeclaration, expression: ts.NewExpression,
                                sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        if (ts.isIdentifier(expression.expression)) {
            let positionInfo = this.getPositionInfo(expression, sourceFile);
            let name = expression.expression.getText();
            this.checkParameter(child, name, positionInfo, arkFile);
        }
        if (ts.isPropertyAccessExpression(expression.expression)) {
            let positionInfo = this.getPositionInfo(expression, sourceFile);
            let name = expression.expression.name.getText();
            const className = ts.isIdentifier(expression.expression.expression) ? expression.expression.expression.getText() : '';
            this.checkPropertyAccessParameter(child, className, name, positionInfo, arkFile);
        }
    }

    private handleTaggedTemplateExpression(child: ts.FunctionDeclaration, expression: ts.TaggedTemplateExpression,
                                           sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        if (ts.isIdentifier(expression.tag)) {
            let positionInfo = this.getPositionInfo(expression.tag, sourceFile);
            let name = expression.tag.getText();
            this.checkParameter(child, name, positionInfo, arkFile);
        }
        if (ts.isPropertyAccessExpression(expression.tag)) {
            let positionInfo = this.getPositionInfo(expression.tag, sourceFile);
            let name = expression.tag.name.getText();
            const className = ts.isPropertyAccessExpression(expression.tag) ? expression.tag.expression.getText() : '';
            this.checkPropertyAccessParameter(child, className, name, positionInfo, arkFile);
        }
    }

    private checkParameter(child: ts.FunctionDeclaration, name: string, positionInfo: PositionInfo, arkFile: ArkFile): void {
        const parameter = child.parameters?.find(param =>
            ts.isParameter(param) && param.name.getText() === name
        );
        if (parameter && parameter.type?.kind === ts.SyntaxKind.AnyKeyword) {
            this.addAstIssueReport(
                arkFile,
                positionInfo.startPosition.line + 1,
                positionInfo.startPosition.character + 1,
                positionInfo.endPosition.character + 1,
                this.metaData.description
            );
        }
    }

    private checkPropertyAccessParameter(child: ts.FunctionDeclaration, className: string, name: string,
                                         positionInfo: PositionInfo, arkFile: ArkFile): void {
        for (const parameter of child.parameters ?? []) {
            if (ts.isParameter(parameter)) {
                this.checkParameterType(parameter, className, name, positionInfo, arkFile);
            }
        }
    }

    private checkParameterType(parameter: ts.ParameterDeclaration, className: string, name: string,
                               positionInfo: PositionInfo, arkFile: ArkFile): void {
        const paramName = parameter.name.getText();
        if (paramName === className && parameter.type?.kind === ts.SyntaxKind.AnyKeyword) {
            this.addAstIssueReport(
                arkFile,
                positionInfo.startPosition.line + 1,
                positionInfo.startPosition.character + 1,
                positionInfo.endPosition.character + 1,
                this.metaData.description
            );
        }
        if (parameter.type && ts.isTypeLiteralNode(parameter.type)) {
            for (const member of parameter.type.members) {
                if (ts.isPropertySignature(member) && member.name!.getText() === name && member.type?.kind === ts.SyntaxKind.AnyKeyword) {
                    this.addAstIssueReport(
                        arkFile,
                        positionInfo.startPosition.line + 1,
                        positionInfo.startPosition.character + 1,
                        positionInfo.endPosition.character + 1,
                        this.metaData.description
                    );
                }
            }
        }
    }

    private checkExpressionStatement(child: ts.ExpressionStatement, sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        if (!child.expression) {
            return;
        }
        const { positionInfo, className, name } = this.extractExpressionInfo(child, sourceFile);
        if (!className) {
            return;
        }
        const findLast = this.findLastParent(child);
        if (!findLast) {
            return;
        }
        for (const node of findLast.getChildren()[0].getChildren()) {
            if (ts.isVariableStatement(node)) {
                for (const declaration of node.declarationList.declarations) {
                    this.checkVariableDeclaration(declaration, className, name, positionInfo, arkFile);
                }
            }
        }
    }

    private extractExpressionInfo(child: ts.ExpressionStatement,
                                  sourceFile: ts.SourceFile): { positionInfo: PositionInfo, className: string, name: string } {
        let positionInfo = this.getPositionInfo(child, sourceFile);
        let className: string = '';
        let name: string = '';
        if (ts.isTaggedTemplateExpression(child.expression)) {
            positionInfo = this.getPositionInfo(child.expression.tag, sourceFile);
            className = child.expression.tag.getText();
            name = ts.isPropertyAccessExpression(child.expression.tag) ? child.expression.tag.name.getText() : '';
        } else if (ts.isCallExpression(child.expression)) {
            positionInfo = this.getPositionInfo(child.expression.expression, sourceFile);
            className = this.getClassNameFromExpression(child.expression.expression);
            name = this.getNameFromExpression(child.expression.expression);
        } else if (ts.isNewExpression(child.expression)) {
            positionInfo = this.getPositionInfo(child.expression, sourceFile);
            className = this.getClassNameFromExpression(child.expression.expression);
            name = this.getNameFromExpression(child.expression.expression);
        }
        return { positionInfo, className, name };
    }

    private checkVariableDeclaration(declaration: ts.VariableDeclaration, className: string,
                                     name: string, positionInfo: PositionInfo, arkFile: ArkFile): void {
        if (declaration.name.getText() === className) {
            this.checkDeclarationType(declaration, positionInfo, arkFile);
        }
        if (declaration.type && ts.isTypeLiteralNode(declaration.type)) {
            this.checkTypeLiteralMembers(declaration.type, name, positionInfo, arkFile);
        }
    }

    private findLastParent(child: ts.Node): ts.Node | null {
        if (!child || !child.parent) {
            return null;
        }
        if (!child.parent.parent) {
            return child.parent;
        }
        return this.findLastParent(child.parent);
    }

    private getClassNameFromExpression(expression: ts.Expression): string {
        if (ts.isIdentifier(expression)) {
            return expression.getText();
        } else if (ts.isPropertyAccessExpression(expression)) {
            return this.getLastExpression(expression).getText();
        } else if (ts.isElementAccessExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
            return expression.expression.expression.getText();
        }
        return '';
    }

    private getNameFromExpression(expression: ts.Expression): string {
        if (ts.isPropertyAccessExpression(expression)) {
            return expression.name.getText();
        } else if (ts.isElementAccessExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
            return expression.expression.name.getText();
        }
        return '';
    }

    private getLastExpression(expression: ts.Expression): ts.Expression {
        if (ts.isPropertyAccessExpression(expression)) {
            return this.getLastExpression(expression.expression);
        } else {
            return expression;
        }
    }

    private checkDeclarationType(declaration: ts.VariableDeclaration, positionInfo: PositionInfo, arkFile: ArkFile): void {
        if (declaration.type?.kind === ts.SyntaxKind.AnyKeyword) {
            this.addAstIssueReport(
                arkFile, positionInfo.startPosition.line + 1,
                positionInfo.startPosition.character + 1,
                positionInfo.endPosition.character + 1,
                this.metaData.description
            );
        }
    }

    private checkTypeLiteralMembers(type: ts.TypeLiteralNode, name: string, positionInfo: PositionInfo, arkFile: ArkFile): void {
        type.members.forEach(member => {
            if (ts.isPropertySignature(member) && member.name!.getText() === name) {
                if (member.type?.kind === ts.SyntaxKind.AnyKeyword) {
                    this.addAstIssueReport(
                        arkFile,
                        positionInfo.startPosition.line + 1,
                        positionInfo.startPosition.character + 1,
                        positionInfo.endPosition.character + 1,
                        this.metaData.description
                    );
                }
            }
        });
    }

    private getPositionInfo(expression: ts.Node, sourceFile: ts.SourceFileLike): {
        startPosition: ts.LineAndCharacter,
        endPosition: ts.LineAndCharacter
    } {
        const start = expression.getStart();
        const end = expression.getEnd();
        const startPositionInfo = sourceFile.getLineAndCharacterOfPosition(start);
        const endPositionInfo = sourceFile.getLineAndCharacterOfPosition(end);
        return {
            startPosition: startPositionInfo,
            endPosition: endPositionInfo
        };
    }

    private addAstIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string) : void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const defect = new Defects(line, startCol, endCol, message, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.defects.push(defect);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}